﻿/*
	wp8libflac project
	© Alovchin, 2014
*/

/*
    This code heavily references FlacBox project's WaveOverFlacStream:
    https://flacbox.codeplex.com/
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage.Streams;
using wp8.Interop;

namespace wp8.MediaHelpers
{
    /// <summary>
    /// Wraps FLAC stream to WAVE data stream.
    /// </summary>
    internal class FlacWaveStream : IInputStream
    {
        private static readonly BufferSegment _noCurrentData = new BufferSegment(WindowsRuntimeBuffer.Create(0));

        private readonly FlacDecoder _flacDecoder;

        private IEnumerator<BufferSegment> _streamIterator;
        private BufferSegment _currentData;
        private bool _isIteratorFinished;
        
        private bool _headerRead;
        private FlacStreamInfo _streamInfo;

        public FlacWaveStream(string fileName)
        {
            this._flacDecoder = new FlacDecoder();
            this._flacDecoder.Initialize(fileName);

            this._streamIterator = this.IterateOverStream();
            this._isIteratorFinished = false;
        }

        public long Position
        {
            get { return this._flacDecoder.Position; }
        }

        /// <summary>
        /// Gets FLAC stream info.
        /// </summary>
        /// <returns>FLAC stream info.</returns>
        /// <exception cref="EndOfStreamException">This stream contains no data.</exception>
        public FlacStreamInfo GetStreamInfo()
        {
            this.EnsureHeaderRead();
            return this._streamInfo;
        }

        /// <summary>
        /// Gets the duration for the specified buffer size.
        /// </summary>
        /// <param name="bufferSize">Buffer size.</param>
        /// <returns>Duration.</returns>
        /// <exception cref="EndOfStreamException">This stream contains no data.</exception>
        public double GetDurationFromBufferSize(uint bufferSize)
        {
            FlacStreamInfo streamInfo = this.GetStreamInfo();

            if (streamInfo.BytesPerSecond == 0)
                return 0;

            return (double) bufferSize/streamInfo.BytesPerSecond;
        }

        /// <summary>
        /// Gets the buffer size for the specified duration.
        /// </summary>
        /// <param name="duration">Duration.</param>
        /// <returns>Buffer size.</returns>
        /// <exception cref="EndOfStreamException">This stream contains no data.</exception>
        public uint GetBufferSizeFromDuration(long duration)
        {
            FlacStreamInfo streamInfo = this.GetStreamInfo();
            return (uint) (duration*streamInfo.BytesPerSecond/10000000);
        }

        public IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options)
        {
            return AsyncInfo.Run<IBuffer, uint>((t, p) => Task.FromResult(this.Read(buffer, count)));
        }

        private IBuffer Read(IBuffer buffer, uint count)
        {
            if (buffer == null)
                throw new ArgumentNullException();

            if (count > buffer.Capacity)
                throw new ArgumentOutOfRangeException();

            this.EnsureHeaderRead();

            if (this._currentData.Count >= count)
            {
                this._currentData.Buffer.CopyTo(this._currentData.Offset, buffer, 0, count);
                this._currentData = new BufferSegment(
                    this._currentData.Buffer, this._currentData.Offset + count, this._currentData.Count - count);
                buffer.Length = count;
                return buffer;
            }

            uint read = this._currentData.Count;
            if (this._currentData.Count > 0)
                this._currentData.Buffer.CopyTo(this._currentData.Offset, buffer, 0, this._currentData.Count);
            this._currentData = _noCurrentData;

            while (this._streamIterator.MoveNext())
            {
                uint rest = count - read;
                if (this._streamIterator.Current.Count >= rest)
                {
                    this._streamIterator.Current.Buffer.CopyTo(0, buffer, read, rest);
                    read += rest;
                    this._currentData = new BufferSegment(this._streamIterator.Current.Buffer, rest,
                        this._streamIterator.Current.Count - rest);
                    break;
                }
                this._streamIterator.Current.Buffer.CopyTo(0, buffer, read, this._streamIterator.Current.Count);
                read += this._streamIterator.Current.Count;
            }

            buffer.Length = read;
            return buffer;
        }

        public void Seek(long offset)
        {
            if (this._isIteratorFinished)
            {
                this._streamIterator.Dispose();
                this._streamIterator = this.IterateOverStream();
            }
            this._streamIterator.MoveNext();

            this._flacDecoder.Seek(offset);
        }

        public void Close()
        {
            this._flacDecoder.Close();
        }

        private IEnumerator<BufferSegment> IterateOverStream()
        {
            this._streamInfo = this._flacDecoder.GetStreamInfo();
            yield return _noCurrentData;

            while (true)
            {
                FlacSample sample = this._flacDecoder.GetSample();

                if (sample != null)
                {
                    yield return new BufferSegment(sample.Buffer);
                }
                else
                {
                    this._isIteratorFinished = true;
                    break;
                }
            }
        }

        private void EnsureHeaderRead()
        {
            if (!this._headerRead)
            {
                if (!this._streamIterator.MoveNext())
                    throw new EndOfStreamException("This stream contains no data.");

                this._currentData = this._streamIterator.Current;
                this._headerRead = true;
            }
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                this._streamIterator.Dispose();
                this._flacDecoder.Dispose();
            }
        }
        
        ~FlacWaveStream()
        {
            this.Dispose(false);
        }
    }
}
